﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using MbUnit.Framework;

namespace Quail
{
    /// <summary>
    /// Standard SAJ Parallel Test fixture
    /// </summary>
    [TestFixture]
    [Parallelizable]
    public abstract class ParallelTestFixture : IDisposable
	{
        private Dictionary<string, EmptyShell> _shells;
        private static readonly object SyncStatic = new object();
        private static readonly Dictionary<string, int> EnvironmentSemaphores = new Dictionary<string, int>();
        private readonly object _syncThis = new object();
        private int? _maxParallelism;
        
        [ThreadStatic]
        private static bool _ignoreStatusInTearDown;

        /// <summary>
        /// Returns the current environment for the test (Dev, QA, UAT, etc)
        /// </summary>
        public abstract string CurrentEnvironment { get; }

        /// <summary>
        /// Returns the maximum parallelism of all tests
        /// </summary>
        public abstract int MaximumParalellism { get; }


        ~ParallelTestFixture()
        {
            Dispose(false);
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <filterpriority>2</filterpriority>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
        }

        /// <summary>
        /// Sets up the test fixture
        /// </summary>
        [FixtureSetUp]
        public virtual void FixtureSetUp()
        {
            _shells = new Dictionary<string, EmptyShell>();
        }

        /// <summary>
        /// Tears down the test fixture
        /// </summary>
        [FixtureTearDown]
        public virtual void FixtureTearDown()
        {
            foreach (var entry in _shells)
            {
                if (entry.Value != null)
                    entry.Value.Dispose();
            }
            _shells.Clear();
            _shells = null;
        }

        /// <summary>
        /// Sets up a test method
        /// </summary>
        [SetUp]
        public virtual void TestSetup()
        {
            _ignoreStatusInTearDown = false;
        }

        /// <summary>
        /// Tears down a test method
        /// </summary>
        [TearDown]
        public virtual void TestTearDown()
        {
            CheckThreadStatus();
        }

        /// <summary>
        /// Gets or sets a value indicating whether to check the status in test teardown causing Gallio to show the test fails.
        /// </summary>
        protected static bool CheckStatusInTearDown
        {
            get
            {
                return !_ignoreStatusInTearDown;
            }
            set
            {
                _ignoreStatusInTearDown = !value;
            }
        }

        /// <summary>
        /// Gets an "empty" container shell 
        /// </summary>
        /// <param name="shellName">Name of the test shell</param>
        /// <returns>An empty test shell</returns>
        public EmptyShell GetContainerShell(string shellName)
        {
            // , object owningInstance
            lock (_shells)
            {
                if (!_shells.ContainsKey(shellName))
                {
                    var s = EmptyShell.Start(shellName, this, CurrentEnvironment, string.Empty);
                    _shells.Add(shellName, s);
                }
                return _shells[shellName];
            }
        }

        /// <summary>
        /// Runs a given test, wrapping the execution of the test with the typical set of code.  Use this version if you are NOT using MbUnit row tests.
        /// </summary>
        /// <typeparam name="T">
        /// The type of shell to create for the execution of the test. This implementation assumes that T can be constructed with the 
        /// following parameters string, object ... if T doesn't support this, this method will not operate correctly.  However, you 
        /// can use the below method to pass your own function in for creating the type.
        /// </typeparam>
        /// <param name="testName">The name of the test that gets logged...  if null, your method name will be used.</param>
        /// <param name="test">The test code to run.</param>
        [DebuggerStepThrough]
        protected void RunTest<T>(string testName, Action<T, Task> test) where T : class, ITestShell
        {
            if (testName == null)
                testName = StackHelper.GetCallingMethodName(1);
            RunTest(
                testName,
                test,
                () => CreateHelper.CreateInstance<T>(testName, this),
                null);
        }

        /// <summary>
        /// Runs a given test, wrapping the execution of the test with the typical set of code.  Use this version if you are NOT using MbUnit row tests.
        /// </summary>
        /// <typeparam name="T">
        /// The type of shell to create for the execution of the test. This implementation assumes that T can be constructed with the 
        /// following parameters string, object ... if T doesn't support this, this method will not operate correctly.  However, you 
        /// can use the below method to pass your own function in for creating the type.
        /// </typeparam>
        /// <param name="test">The test code to run.</param>
        [DebuggerStepThrough]
        protected void RunTest<T>(Action<T, Task> test) where T : class, ITestShell
        {
            var testName = StackHelper.GetCallingMethodName(1);
            RunTest(
                testName,
                test,
                () => CreateHelper.CreateInstance<T>(testName, this),
                null);
        }

        /// <summary>
        /// Runs a given test, wrapping the execution of the test with the typical set of code.  Use this version only if you are using MbUnit row tests.
        /// </summary>
        /// <typeparam name="T">
        /// The type of shell to create for the execution of the test. This implementation assumes that T can be constructed with the 
        /// following parameters string, object ... if T doesn't support this, this method will not operate correctly.  However, you 
        /// can use the below method to pass your own function in for creating the type.
        /// </typeparam>
        /// <param name="rowName">The name of the test being run.</param>
        /// <param name="test">The test to run.</param>
        [DebuggerStepThrough]
        protected void RunRowTest<T>(string rowName, Action<T, Task> test) where T : class, ITestShell
        {
            // create an empty container shell to act as a grouping mechanism for the actual test shells
            var methodName = StackHelper.GetCallingMethodName(1);

            EmptyShell containershell = GetContainerShell(methodName);

            string fullTestName = string.Format("{0} - {1}", methodName, rowName);

            // actual execution of test
            RunTest(
                fullTestName,
                test,
                () => CreateHelper.CreateInstance<T>(fullTestName, containershell),
                containershell.ActiveTask);
        }

        /// <summary>
        /// Runs a given test, wrapping the execution of the test with the typical set of code.
        /// </summary>
        /// <remarks>
        /// This accomplishes the same as the above, but with two differences: 
        ///     first, it no longer requires each implementation of SAJParallelTestFixture to create a CreateShell method (it allows it to specified inline)
        ///     secondly, it allows the Action that is being run to get the actual type of the Shell directly, without the need to cast/check type.
        /// </remarks>
        /// <typeparam name="T">The type of shell to create for the execution of the test.</typeparam>
        /// <param name="testName">The name of the test being run.</param>
        /// <param name="test">The test implementation to run.</param>
        /// <param name="createShell">The method to run to create a shell</param>
        /// <param name="parentTask">The parent task to begin</param>
        [DebuggerStepThrough]
        protected static void RunTest<T>(string testName, Action<T, Task> test, Func<T> createShell, Task parentTask) where T : class, ITestShell
        {
            // create the actual test shell
            using (T shell = createShell())
            {
                if (Equals(default(T), shell))
                {
                    throw new NotImplementedException(
                        string.Format(
                            "RunTest was unable to create a new instance of '{0}'. Does it offer a constructor that takes string, object?",
                            typeof (T).Name));
                }

                shell.Start(parentTask);
                shell.ActiveTask.RunTask(testName, task => test(shell, task));
            }

            CheckThreadStatus();
        }

        private static void CheckThreadStatus()
        {
            if (!_ignoreStatusInTearDown && (Task.ThreadState.IsFailingStatus() || Task.ThreadState == LogStatus.Abandoned))
            {
                _ignoreStatusInTearDown = true;
                Assert.Fail("Failing Task state found: " + Task.ThreadState);
            }            
        }


        /// <summary>
        /// Sets the degree of parallelism (number of simultaneous tests) across all test assemblies
        /// </summary>
        /// <param name="max">the maximum of number of tests for the environment</param>
        /// <param name="environment">The environment</param>
        private static int GetLimitedParallelism(int max, string environment)
        {
            lock (SyncStatic)
            {
                var env = environment ?? string.Empty;
                int sem;
                if (!EnvironmentSemaphores.TryGetValue(env, out sem))
                {
                    if (max == -1)
                        sem = int.MaxValue;
                    else if (max < 20)
                        sem = 20;
                    else if (max > 500)
                        sem = 500;
                    else
                        sem = max;

                    EnvironmentSemaphores.Add(env, sem);
                }

                return sem;
            }
        }

        /// <summary>
        /// This setup method enforces Parallelism across all test assemblies
        /// </summary>
        [SetUp]
        [Timeout(30 * 60)] // 1 hour
        public void EnforceParallelism()
        {
            return; // disabling EnforceParallelism

            if (!QuailAssemblyTestFixture.IsRunningUnderGallio)
                return;

            lock (_syncThis)
            {
                if (_maxParallelism == null)
                    _maxParallelism = GetLimitedParallelism(MaximumParalellism, CurrentEnvironment);
            }

            var env = QuailEnvironmentConfiguration.Environment.GetEnvironment(CurrentEnvironment);
            if (env != null)
            {
                var sw = new Stopwatch();
                sw.Start();
                // wait up to 25 minutes for the test counts to die down
                while (env.GetCurrentActiveTestCount() >= _maxParallelism.Value && sw.ElapsedMilliseconds < 25 * 60 * 1000)
                {
                    // wait 10 seconds
                    Thread.Sleep(10100);
                }
            }
        }

        /// <summary>
        /// This teardown method reduces the parallelism for all test assemblies
        /// </summary>
        [TearDown]
        public void ReduceParallelism()
        {
            // nothing to do
        }
	}
}
